Repository navigation
Rewrite publisher origin URLs in proxied response headers - #1238
Conversation
Publisher pages had origin URLs rewritten in the body but not in headers, so browsers followed Location and Refresh to the origin, preloaded Link targets from it, and blocked rewritten resources under the forwarded CSP. Map origin URLs in Location, Content-Location, Refresh, and Link (including imagesrcset) to the serving host on every publisher response, and add serving-host sources beside origin sources in Content-Security-Policy and its report-only variant. Rewriting runs before the template-cache gate so cached templates replay the rewritten policy headers; bump the template schema to 6 so entries stored with origin-only policy metadata are not replayed. Leave a Location or Refresh target unchanged when it names the current request URL under a different scheme than the origin, so an origin scheme upgrade cannot loop the browser. Closes #1128 Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Solid, well-tested change. Rewrites origin URLs in Location, Content-Location, Refresh, Link, and both CSP headers; Set-Cookie/Vary untouched. Host matching is strict and case-insensitive, multi-value headers preserve order, and the rewrite runs before template-cache capture (with the schema bump to 6) in the shared handle_publisher_request path used by all adapters. No blocking findings.
Non-blocking
- 🤔 Loop-guard escape logs only at
debug(inline, suggestion) - 🤔 Same-URL cookie-setting redirect can loop when the cookie has
Domain=<origin host>(inline) - 🌱 CSP
:*/ explicit default-port sources not matched (inline) - ⛏ Docs row omits
report-to(inline, suggestion)
📌 Out of scope
Set-Cookie Domain, Reporting-Endpoints/Report-To, and Access-Control-Allow-Origin still pass through unchanged. Leaving report endpoints alone matches the report-uri choice; the Set-Cookie Domain gap (see inline comment) is worth a follow-up issue. Asset-proxy routes (handle_asset_proxy_request) also bypass this rewrite, consistent with the docs saying "publisher response".
📝 Note
The PR checklist says "Uses tracing macros", but the code correctly uses log — only the checklist wording is off.
Verification
cargo fmt --all -- --check✅cargo clippy-fastly✅ (no warnings)cargo test -p trusted-server-core(host) ✅ 2916 passed, 0 failed (includes 18 newresponse_header_rewritetests and 5 new publisher tests)- Both suggestions scratch-verified (fmt/clippy/tests; docs prettier check)
- CI: all 20 checks passing
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Review summary
Reviewed 7871acdd56324d20c182e0c3093e5bd535dd96e9 against 80483011e30a446ac741b4423e36f8d142c5d2ac, branch fix/rewrite-origin-urls-in-response-headers into main. Approving with two P2 edge-case findings posted inline.
Inspected all five changed files and traced the rewrite through publisher response modes, cache capture/replay, all four adapters, and final header overrides. Cache-hit, repeated-policy-header, and previous-schema rejection tests confirm rewritten policy replay for the tested core cache implementation.
Validation and review context
cargo test -p trusted-server-core response_header_rewrite --locked: 18 passed. Initial compilation timed out; rerun completed.cargo test -p trusted-server-core publisher::tests --locked: 372 passed.cargo test -p trusted-server-core platform::template_cache --locked: 32 passed.cargo test -p trusted-server-core --locked --quiet: 2,916 unit tests passed; 4 doctests passed, 5 ignored.cargo fmt --all -- --check: passed.- An exact-module scratch executable and two
node --input-type=moduleChromium fixtures reproduced the inline findings. - CI: All 20 reported checks passed; none reported failed or skipped.
- Existing feedback: Inspected the review, inline comments, unresolved threads, replies, and issue comments. Neither finding duplicates existing feedback.
- Residual risk: Browser fixtures exercised the exact rewrite module, not a deployed adapter. Adapter suites, production Fastly cache, and lint checks were not rerun locally.
- Revision remained unchanged; working tree is clean. No repository files were edited and no review work was delegated.
A scheme-change redirect such as https://origin.example.com?x=1 or one with dot segments was compared to the request path as raw text, so it did not match /?x=1 and was rewritten back to the URL the browser had just requested, looping. Parse the target as a URL so the comparison sees the same path and query the browser will follow. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Deduplication compared whole source expressions case-insensitively, so a policy listing origin paths /A.js and /a.js gained a serving-host sibling only for /A.js and the rewritten /a.js stayed blocked. Compare scheme and host case-insensitively and the path exactly, as CSP path matching does. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
When the origin forces a different scheme than publisher.origin_url, every navigation leaves the serving host and bypasses Trusted Server. That almost always means origin_url has the wrong scheme, so surface it as a warning naming the setting to check. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Same-scheme redirects to the current URL are now kept on the serving host, but Set-Cookie Domain attributes are not rewritten. A cookie scoped to the origin host is rejected on the serving host, so a cookie-gated self-redirect repeats. State this in the navigation rewrite docs and the configuration guide. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…ls-in-response-headers Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
aram356
left a comment
There was a problem hiding this comment.
Summary
Rewrites publisher-origin URLs in Location, Content-Location, Refresh, Link, and CSP headers so the browser stays on the serving host (closes #1128). The rewrite holds up in a real browser, but the branch now conflicts with main after #1210, and #1210 changes the right resolution for the schema bump.
2 of the inline comments below carry a one-click GitHub
suggestion. Use Commit suggestion (or Add suggestion to batch for both) to apply them as commits on the PR branch. The remaining comments describe the fix in prose because the change touches multiple files or lines outside the diff and can't be auto-applied.
Blocking
🔧 wrench
- Conflicts with main; drop the v6 schema bump: see inline at
crates/trusted-server-core/src/platform/template_cache.rs:41
Non-blocking
🤔 thinking / 📌 out of scope / ⛏ nitpick
- 🤔 A Host override turns a canonical-host redirect into a loop: see inline at
crates/trusted-server-core/src/response_header_rewrite.rs:92 - 🤔 A serving-host CSP source also allows
/first-party/proxy(suggestion): see inline atdocs/guide/configuration.md:828 - 📌
<meta http-equiv>refresh and CSP in the page body are not rewritten: see inline atdocs/guide/configuration.md:783 - ⛏ Allocate lazily in
rewrite_link(suggestion): see inline atcrates/trusted-server-core/src/response_header_rewrite.rs:296 - ⛏ Reuse
request_path_and_query: see inline atcrates/trusted-server-core/src/publisher.rs:4523
Cross-cutting / body-level findings
-
🏕 CHANGELOG entry: operators upgrading will see changed
Locationvalues and wider CSP headers on proxied pages. A suggested line under### Fixed:- Publisher responses now map origin URLs in
Location,Content-Location,Refresh, andLinkheaders to the serving host, and each CSP source naming the origin gains a serving-host source beside it. A redirect to the current URL under a different scheme is left unchanged to avoid a loop. See "Origin URLs in proxied response headers" in the configuration guide.
- Publisher responses now map origin URLs in
Verification
- Browser checks from the test plan's unchecked item, run with the Axum dev server and Chrome at
0d0689aca.Location,Refresh, and theLinkpreload stay on the serving host, and the origin log shows no direct hits. In the CSPimg-srccase, the rewritten image loads with no violation. The scheme-upgrade redirect is left unchanged, with one hop and no loop. On main, the same headers pass through unrewritten. - A scratch merge with
origin/main(da31a215e), resolved as described in the 🔧 comment, passes fmt, clippy-fastly, test-fastly (2,919 core tests), test-axum, test-cloudflare, test-spin, and parity. - Both suggestions are scratch-verified. The docs one passes the docs prettier check. The Rust one passes fmt, all six adapter clippy aliases, the four adapter test suites, and parity.
CI Status
All checks ran on head 0d0689aca, which predates #1210. Nothing has run on a merge with current main.
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- Analyze (javascript-typescript): PASS
- Analyze (python): PASS
- Analyze (rust): PASS
- browser integration tests: PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- CLAUDE.md symlink guard: PASS
- CodeQL: PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- vitest: PASS
Keep template schema version 5 from main. The core build digest added in #1210 already invalidates templates stored before the response header rewrite, so the version 6 bump is no longer needed. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
A GET or HEAD redirect to the current URL that sets no cookie loops through Trusted Server when the origin redirects on something sent unchanged, such as an origin Host override. Leave its Location unchanged and log a warning. Also allocate lazily when rewriting Link values, reuse the captured request path and query, document the meta tag gap and the proxy reach of serving-host CSP sources, and add a changelog entry. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Brings #1238 at d824a42. Integration decisions: - CHANGELOG.md, lib.rs and publisher.rs imports keep both sides: the RC's request_timing module and #1238's response_header_rewrite module sit side by side. - The header rewrite runs where #1238 placed it, after the RC's trace extension and unexpected-304 handling and before the template-cache gate.
aram356
left a comment
There was a problem hiding this comment.
Summary
Round 2 at d824a42d3. Every round-1 finding is addressed. The schema bump is gone, the CSP warning and meta-tag note are in the guide with #1265 tracking the meta tags, both nits are applied, and the CHANGELOG has an entry. The new loop guard fixes the round-1 repro, but only when the redirect sets no cookie, so the same loop comes back for origins that set a cookie on every response.
1 of the inline comments below carries a one-click GitHub
suggestion. Use Commit suggestion to apply it as a commit on the PR branch. The remaining comments describe the fix in prose because the change touches multiple files and can't be auto-applied.
Blocking
🔧 wrench
- A cookie on the canonical-host redirect reopens the override loop: see inline at
crates/trusted-server-core/src/response_header_rewrite.rs:130
Non-blocking
📝 note / ⛏ nitpick
- 📝 Conflicts with main in
CHANGELOG.mdonly; keep both lines: see inline atCHANGELOG.md:36 - ⛏ No test covers the
HEADhalf ofis_get_or_head(suggestion): see inline atcrates/trusted-server-core/src/publisher.rs:16434
Verification
- Browser checks with the Axum dev server and Chrome at
d824a42d3:- The round-1 cases still pass.
Location,Refresh, and theLinkpreload stay on the serving host, the CSPimg-srcimage loads, and the scheme-upgrade redirect is left unchanged after one hop. - The new guard works for a cookieless canonical-host 301 and for
HEAD. - A cookie-gate redirect without an override is still rewritten and ends at 200 on the serving host.
- A POST-redirect-GET stays on the serving host, and an origin self-loop leaves after one hop through TS.
- The round-1 cases still pass.
- A scratch merge with
origin/main(9cf305589), resolved as in the 📝 comment, passes every CI-equivalent gate. - The suggestion is scratch-verified. It passes fmt, all six clippy aliases, the four adapter test suites, and parity, and it fails under the
is_getmutation.
CI Status
All checks ran on head d824a42d3, which merged main at 695ab669a. Main has moved since, and nothing has run on a merge with current main.
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- Analyze (javascript-typescript): PASS
- Analyze (python): PASS
- Analyze (rust): PASS
- browser integration tests: PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- CLAUDE.md symlink guard: PASS
- CodeQL: PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- vitest: PASS
…cookie When publisher.origin_host_header_override sends a Host that differs from the origin host, a Location naming the origin host and the current path is a host change the retry cannot satisfy, so cookies such as load-balancer stickiness no longer re-enable the rewrite. Also cover HEAD in the cookieless redirect test. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Log a separate warning for each reason an origin redirect to the current URL is kept, so an operator can tell a Host-override mismatch from a cookieless self-redirect. Rewrap the loop-exception paragraph in the configuration guide. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The origin redirect design describes the header rewrite merged in main (IABTechLab#1238) with the origin folder that IABTechLab#1378 gives it, in place of the Location rule IABTechLab#1378 no longer has, and records the five deliberate faults its tests catch. The object storage design's sentence on redirects names the mapping it now follows. The same text is on the branch of IABTechLab#1378.
…ect rule The header rewrite merged in main (IABTechLab#1238) maps an origin URL in Location, Content-Location, Refresh, Link and CSP to the serving host, on every route of handle_publisher_request. Its loop exceptions, and its rules for a Location on any status and under a Host override, differ from the Location rule this change added. With both in place the two loop tests from IABTechLab#1238 failed, and so did three of this change's own tests. This change now follows the header rewrite. Its own rule, rewritten_origin_redirect, the call to it and its unit tests are removed. The header rewrite gains origin_path, so with a folder in origin_url a URL on the origin host is mapped only when it is in the folder, and it loses the folder. Dot segments are resolved first, the folder counts only where it ends at a slash, and the loop exceptions compare the reader's path with the URL's path without the folder. A CSP source with no path still gains the whole serving host. The folder cases of the removed unit tests move to the header rewrite's tests. The serve middleware headers step now runs after the header rewrite, so a middleware sees the headers the reader receives and a header it sets is the one sent. Two handler tests take the header rewrite's behavior. A Location on an answer that is not a redirect is named for the reader, and under a Host override the host of origin_url is the origin's name. cargo test -p trusted-server-core --lib -- redirect response_header_rewrite passes 58 tests. The same run on this branch before the change, with IABTechLab#1238 merged, failed five.
The changelog entry, the configuration guide and the origin redirect design describe the header rewrite merged in main (IABTechLab#1238) with the origin folder this change gives it, in place of the Location rule the change no longer has. The guide's section on origin URLs in response headers says how a folder is mapped, and the override is no longer named as the name a redirect is compared with. The design records five deliberate faults, each caught by a test. The object storage design's sentence on redirects names the mapping it now follows.
The header rewrite merged in main (IABTechLab#1238) names publisher.origin_url and publisher.origin_host_header_override in two warnings, a doc comment and the configuration guide, and its handler tests set those fields. This change moves both settings to source.origin.url and source.origin.host_header, so the warnings, the comment and the guide name the new settings and the tests set them through edit_origin, as the change's own tests do. Without the test change core's tests do not compile here. cargo test -p trusted-server-core --lib -- redirect response_header_rewrite passes 58 tests.
The handler tests of the header rewrite merged in main (IABTechLab#1238) build their request with fastly-ssl: 1 to mark it HTTPS. This change takes that header out of core, so core reads the request as plain HTTP and three of those tests receive http:// where they expect https://. The core platform name check also fails on the header's name. The helper now sends x-forwarded-proto: https, which core reads on every platform and which the origin redirect tests already use. python scripts/lint-core-platform-names.py passes, and the three tests, transformed_page_headers_map_origin_urls_to_serving_host, bodiless_redirect_location_maps_origin_url_to_serving_host and same_scheme_redirect_to_current_url_stays_on_serving_host, pass.
Summary
Location/Refreshoff the appliance, preloadedLinktargets straight from the origin, and blocked rewritten resources under a CSP that only allowed the origin.Location,Content-Location,RefreshandLink(includingimagesrcset) are rewritten. For CSP, a serving-host source is added beside each origin source, and no origin source is removed. Each response's own header values are rewritten, so per-page policies survive; this is not one static replacement.http://origin forcing HTTPS) is left unchanged so it can't loop. A same-schemeGET/HEADredirect to the current URL is also left unchanged when it sets no cookie, or, whatever cookies it sets, whenpublisher.origin_host_header_overridesends a differentHost. Either case is logged atwarn, with a separate message per reason. Same-scheme redirects to the current URL after a form POST, and cookie-setting redirects without aHostoverride, are still rewritten.Changes
crates/trusted-server-core/src/response_header_rewrite.rsLocation,Content-Location,Refresh,Link(<target>and quotedimagesrcset) and CSP/CSP-Report-Only. CSP is additive with de-duplication, andreport-uri/report-toare skipped. Includes the scheme-change and same-URLGET/HEADloop guards (cookieless, or under aHostoverride) and unit tests.crates/trusted-server-core/src/publisher.rsGETredirects to the current URL, and a template-cache hit replaying the rewritten CSP/Link. Reuses the captured request path and query for the readthrough reader URL and the template key.crates/trusted-server-core/src/lib.rspub(crate)).docs/guide/configuration.md<meta http-equiv>refresh/CSP tags in the body are not rewritten, that a serving-host CSP source also allows/first-party/proxy, that[response_headers]still overrides the rewrite, and that host rewriting cannot authorize inline inserts that lack a nonce.Closes
Closes #1128
Spec
Spec: Not a feature
Test plan
cargo test-fastly && cargo test-axum(alsocargo test-cloudflare,cargo test-spin, and theparityintegration test)cargo clippy-fastly && cargo clippy-axum(alsoclippy-cloudflare,clippy-cloudflare-wasm,clippy-spin-native,clippy-spin-wasm)cargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest run(Node 24.12.0, per.tool-versions)cd crates/trusted-server-js/lib && npm run formatcd docs && npm run format(plus the markdown check outsidedocs/)cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveNot yet done: re-running the issue's browser checks (Chrome against the Axum dev server) for
Location,Refresh,Linkpreload and the CSPimg-srccase.Checklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)